package sf

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

const (
	defaultBaseURL = "https://commit-openic.sf-express.com/"
	userAgent      = "go-sf"
)

// Config 顺丰配置
type Config struct {
	DevID  int    `mapstructure:"dev_id"`
	DevKey string `mapstructure:"dev_key"`
}

// Client manages communication with the dada API.
type Client struct {
	client *http.Client

	DevID  int
	DevKey string
	// Base URL for API.requests.
	// BaseURL should always be specified with a trailing slash.
	BaseURL *url.URL

	// User agent used when communication with dada API.
	UserAgent string

	common service // Reuse a single struct instead of allocating one for each service on the heap.

	// Services used for talking to different parts of dada API.
	Order *OrderService
}

type service struct {
	client *Client
}

// NewClient returns a new sf API client.
func NewClient(devID int, devKey string) *Client {
	baseURL, _ := url.Parse(defaultBaseURL)
	c := Client{BaseURL: baseURL,
		UserAgent: userAgent,
		client:    &http.Client{},
		DevID:     devID,
		DevKey:    devKey,
	}
	c.common.client = &c
	c.Order = (*OrderService)(&c.common)
	return &c
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// If specified, the value pointed to by body is JSON encoded and included as
// request body.
func (c *Client) NewRequest(urlStr string, body interface{}) (*http.Request, error) {
	u, err := c.BaseURL.Parse(urlStr)
	if err != nil {
		return nil, err
	}

	var rawBody string
	var buf bytes.Buffer
	var data []byte
	if body != nil {
		enc := json.NewEncoder(&buf)
		enc.SetEscapeHTML(false)
		if err := enc.Encode(body); err != nil {
			return nil, err
		}
		data, _ = json.Marshal(body)
		rawBody = string(data)
	}
	var sign string
	if sign, err = c.Sign(rawBody); err != nil {
		return nil, err
	}
	q, _ := url.ParseQuery(u.RawQuery)
	q.Add("sign", sign)
	u.RawQuery = q.Encode()
	req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(data))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/json")

	if c.UserAgent != "" {
		req.Header.Set("User-Agent", c.UserAgent)
	}
	return req, nil
}

// Sign 参数签名
func (c *Client) Sign(rawBody string) (string, error) {
	rawString := strings.Join([]string{rawBody, strconv.Itoa(c.DevID), c.DevKey}, "&")
	hash := md5.Sum([]byte(rawString))
	return base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(hash[:]))), nil
}

// Response represents dada Response.
type Response struct {
	*http.Response

	ErrorCode int             `json:"error_code"`
	ErrorMsg  string          `json:"error_msg"`
	ErrorData json.RawMessage `json:"error_data"`
	Result    json.RawMessage `json:"result"`
}

// ErrorResponse represents dada Error Response.
type ErrorResponse struct {
	*http.Response
	requestBody []byte

	ErrorCode int             `json:"error_code"`
	ErrorMsg  string          `json:"error_msg"`
	ErrorData json.RawMessage `json:"error_data"`
}

// Error implements builtin.error interface.
func (r *ErrorResponse) Error() string {
	return fmt.Sprintf("%v %v body %s : %d %v %s",
		r.Response.Request.Method, r.Response.Request.URL, r.requestBody,
		r.ErrorCode, r.ErrorMsg, r.ErrorData)
}

func newResponse(r *http.Response) (*Response, error) {
	response := Response{Response: r}
	data, err := ioutil.ReadAll(r.Body)
	if err == nil && data != nil {
		// ignore err because maybe image response
		_ = json.Unmarshal(data, &response)
	}
	r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
	return &response, nil
}

// CheckResponse checks the API response for errors, and returns them if present.
// API error responses are expected to have response body.
// If none error find, return dada.Response.
func CheckResponse(r *Response, reqBody []byte) error {
	errResponse := &ErrorResponse{
		Response:    r.Response,
		requestBody: reqBody,
		ErrorCode:   r.ErrorCode,
		ErrorMsg:    r.ErrorMsg,
		ErrorData:   r.ErrorData,
	}
	switch errResponse.ErrorCode {
	case 0:
		return nil
	default:
		return errResponse
	}
}

// Do Sends an API request and returns the API response. The API response is
// JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred. If v implements the io.Writer interface,
// the raw response body will be written to v, without attempting to first
// decode it.
//
// The provided ctx must be non-nil, if it is nil an error is returned. If it
// is canceled or timeout, ctx.Err() will be returned.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
	if ctx == nil {
		return nil, errors.New("context must be non-nil")
	}

	req = req.WithContext(ctx)

	// save body for display request error info.
	body, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return nil, errors.New("read request body error")
	}

	req.Body = ioutil.NopCloser(bytes.NewBuffer(body))

	resp, err := c.client.Do(req)
	if err != nil {
		// If we got an error, and the context has been canceled,
		// the context's error id probably more useful.
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}
		return nil, err

	}

	response, err := newResponse(resp)
	if err != nil {
		return nil, err
	}

	if err := CheckResponse(response, body); err != nil {
		return resp, err
	}

	if v != nil {
		if w, ok := v.(io.Writer); ok {
			io.Copy(w, resp.Body)
		} else {
			var data io.Reader
			if len(response.Result) > 0 {
				data = bytes.NewReader(response.Result)
			} else {
				data = resp.Body
			}
			decErr := json.NewDecoder(data).Decode(v)
			if decErr == io.EOF {
				decErr = nil // ignore io.EOF errors caused by empty response body
			}
			if decErr != nil {
				err = decErr
			}
		}
	}
	return resp, err

}
